Skip to content

fix(confirm-write): render write args as a table, and label buttons by verb - #19

Merged
jerelvelarde merged 3 commits into
mainfrom
jerel/confirm-write-table
Aug 3, 2026
Merged

jerelvelarde merged 3 commits into
mainfrom
jerel/confirm-write-table

Conversation

@jerelvelarde

Copy link
Copy Markdown
Collaborator

Fixes the confirm_write approval card, which serialized the mutating tool's arguments with json.dumps and rendered them as a raw JSON line. For a Linear project write that was ~15 fields long enough that Slack collapsed it behind Show more — so the approver could not see the payload they were authorizing. That defeats the point of the human-in-the-loop gate.

Reported in Slack; tracked as CPK-7748.

Before / after

The exact payload from the report, 15 fields, collapsed mid-blob:

{"addInitiatives": [], "addTeams": ["CopilotKit"], "color": "", "description": "Project for
OpenTag work.", "icon": "Rocket", "id": "", "labels": [], "lead": "jerel@copilotkit.ai",
"name": "OpenTag", "priority": 0, "removeInitiatives": [], "removeTeams": [], "se…

now renders as a native table of 7 rows:

Add teams CopilotKit
Description Project for OpenTag work.
Icon Rocket
Lead jerel@copilotkit.ai
Name OpenTag
Priority 0
Status Backlog

How it works

The agent decides what to show; the card decides how. summarize_args in agent/write_confirmation.py filters and stringifies; ConfirmWrite renders. Both are independently testable and the card stays presentational.

  • Empty values are dropped — "", [], {}, None. The check is explicit membership, not Python falsiness, so priority: 0 and False survive. Dropping numeric zero would silently misrepresent a real Linear value ("No priority").
  • Keys are humanized — addTeams → "Add teams".
  • Rows cap at 12 with a … | N more fields note, so nothing is withheld silently.
  • <Table> is native on both surfaces — a Block Kit table block on Slack, an Adaptive Card Table on Teams. Headerless, since a "Field | Value" header row would spend a row restating the layout.
  • The legacy detail string is still accepted, so a stale agent revision renders a body rather than an empty card across a deploy skew.

Delete correctness

Checking the table against a delete surfaced a second, worse problem. Deletes go through the same interceptor, so delete_customer rendered a green Create button under "Nothing is written until you click Create" — inviting styling and false wording in front of an irreversible operation.

Action Buttons Lock line
Create issue Create primary, Cancel danger Nothing is changed until you click Create
Update issue Update primary, Cancel danger …click Update
Delete customer Delete danger, Cancel neutral …click Delete
Archive project Archive danger, Cancel neutral …click Archive
Cancel subscription Confirm danger, Cancel neutral …click Confirm
  • The button and lock line take the action's own leading verb.
  • "Nothing is written" → "Nothing is changed", true of deletes and updates too.
  • Destructive verbs put the warning colour on confirm and drop Cancel to neutral, so the red marks the irreversible choice rather than the safe one.
  • Cancel subscription would derive the label "Cancel" and render two identical buttons, one cancelling the subscription and one cancelling the request. That case falls back to Confirm, and the destructive flag reads the real verb rather than the substituted label so relabelling cannot silently cost the styling.

Tests

11 new tests, TDD throughout — every one watched failing first, for the right reason.

  • Python: omission policy, 0/False retention, key humanizing, list joining, nested compaction, truncation, the row cap
  • TypeScript: Slack table shape, Teams Adaptive Card with firstRowAsHeader: false, legacy detail fallback, schema rejection of malformed fields, channel wiring, verb labelling, destructive styling, the Cancel collision

CI will be red — not from this change

main does not pass its own suite after the 0.6.0 / 1.65.0 bump: 9 failing tests and 20 type errors on clean b474c17 once pnpm install runs. Filed as CPK-7751 with three root causes (a stale version-pin guard, removed/renamed channels type surface, and a new Intelligence API key format check).

Verified by stashing: the same 9 failures and 20 type errors before and after this branch. This PR adds zero new ones — 130 → 141 passing, 20 → 20 type errors.

Not in scope

  • The non-functional Create button — Tyler is on it.
  • Resolving opaque ids to names. A delete usually carries only {"id": "3f34e790-…"}, so the table honestly shows Id | 3f34e790-… — readable, but it doesn't say what is being deleted. Fixing that needs the agent to resolve the id before pausing, which is agent-side work. Worth its own ticket.

The confirm_write card serializes request.args with json.dumps, so the
approver sees a ~15-field JSON line that Slack collapses behind Show
more -- they cannot read the payload they are authorizing.

Design a Python/TypeScript seam where the agent decides which fields to
show and the card decides how, rendering a headerless Table that both
the Slack and Teams renderers support natively.

Refs CPK-7748
The approval card serialized request.args with json.dumps, so a Linear
project write rendered as a ~15-field JSON line that Slack collapsed
behind Show more. An approver who cannot read the payload cannot
meaningfully approve it, which defeats the human-in-the-loop gate.

Split the concern: summarize_args decides which fields are worth showing
and stringifies them, ConfirmWrite decides how they look. Empty values
are dropped by an explicit membership test rather than falsiness, so a
priority of 0 and a False flag survive.

The card renders a headerless two-column Table, which both the Slack
Block Kit and Teams Adaptive Card renderers support natively. The legacy
detail string stays accepted so a stale agent revision still renders a
body across a deploy skew.

For the payload in the report, 15 fields become 7 readable rows.

Refs CPK-7748
Deletes go through the same interceptor as creates, so a delete_customer
call rendered a green Create button under "Nothing is written until you
click Create" -- inviting styling and false wording in front of an
irreversible operation.

Derive the button and lock line from the action's leading verb, and
reword to "Nothing is changed", which holds for deletes and updates too.
Destructive verbs take the warning colour and drop Cancel to neutral so
the red marks the irreversible choice rather than the safe one.

"Cancel subscription" derives the label Cancel and would render two
identical buttons, one cancelling the subscription and one cancelling the
request; that case falls back to Confirm. The destructive flag reads the
real verb rather than the substituted label, so relabelling cannot
silently cost the styling.

Refs CPK-7748
@jerelvelarde
jerelvelarde merged commit db79d65 into main Aug 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants